﻿using System;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Collections;
using System.Drawing;
using System.Linq;
using System.Workflow.ComponentModel;
using System.Workflow.ComponentModel.Design;
using System.Workflow.ComponentModel.Compiler;
using System.Workflow.ComponentModel.Serialization;
using System.Workflow.Runtime;
using System.Workflow.Activities;
using System.Workflow.Activities.Rules;
using System.Diagnostics;

namespace CommonWFLibrary
{
    /// <summary>
    /// This class will run an external program. If the WF crashes, it will check to see if the
    /// program is still running and, in that case, will immediately start waiting for it to
    /// complete again. This means long running external programs can stay up while the WF host
    /// goes down.
    /// 
    /// WARNING: TimesToRetry must be non-zero to enable this capability. The TimesToRetry controls how many
    /// times the activity gets a chance to recover from a host crash. If it is set to one, and the host goes
    /// down once, then it will do its best to recover that one time.
    /// 
    /// If RestartExternalProgram is true, then if when the activity inspects for an already running program,
    /// and doesn't find it, it will try to re-run it. Otherwise it will throw an exception.
    /// </summary>
	public partial class ExternalProgramActivity: LongRunningActivityBase
	{
        /// <summary>
        /// Create the activity
        /// </summary>
		public ExternalProgramActivity()
		{
            /// If this isn't turned on, then it is not possible
            /// for this guy to ever recover... Set it to zero if
            /// that is the behavior you want.
            TimesToRetry = 1;
        }

        /// <summary>
        /// The path to the program we are supposed to be running
        /// </summary>
        public string ProgramPath
        {
            get { return (string)GetValue(ProgramPathProperty); }
            set { SetValue(ProgramPathProperty, value); }
        }

        public static readonly DependencyProperty ProgramPathProperty =
            DependencyProperty.Register("ProgramPath", typeof(string), typeof(ExternalProgramActivity));


        /// <summary>
        /// Get/Set the argument string to pass to the external program.
        /// </summary>
        public string ProgramArguments
        {
            get { return (string)GetValue(ProgramArgumentsProperty); }
            set { SetValue(ProgramArgumentsProperty, value); }
        }

        // Using a DependencyProperty as the backing store for ExternalProgramArguments.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ProgramArgumentsProperty =
            DependencyProperty.Register("ProgramArguments", typeof(string), typeof(ExternalProgramActivity));

        /// <summary>
        /// Get the final status of the program - its exit code.
        /// </summary>
        public int FinalStatus
        {
            private get { return (int)GetValue(FinalStatusProperty); }
            set { SetValue(FinalStatusProperty, value); }
        }

        // Using a DependencyProperty as the backing store for FinalStatus.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty FinalStatusProperty =
            DependencyProperty.Register("FinalStatus", typeof(int), typeof(ExternalProgramActivity));

        /// <summary>
        /// If false, then we won't restart a program if it exited while the WF host was down.
        /// </summary>
        public bool AllowRestart
        {
            get { return (bool)GetValue(AllowRestartProperty); }
            set { SetValue(AllowRestartProperty, value); }
        }

        // Using a DependencyProperty as the backing store for AllowRestart.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty AllowRestartProperty =
            DependencyProperty.Register("AllowRestart", typeof(bool), typeof(ExternalProgramActivity), new PropertyMetadata(true));

        /// <summary>
        /// Store the external program info so we can look it up if we "crash".
        /// TODO: make all of these internal or private classes!
        /// </summary>
        [Serializable]
        public class EPContext
        {
            /// <summary>
            ///  Id of Running process...
            /// </summary>
            public int ID;
        }

        /// <summary>
        /// The specs and info on starting the program.
        /// </summary>
        [Serializable]
        public class EPStartupInfo
        {
            public string _path;
            public string _arguments;
            public bool _can_restart;
        }

        /// <summary>
        /// Holds the data that is the result from the run - to be put back into the activity
        /// </summary>
        [Serializable]
        public class EPResult
        {
            public int _result;
        }

        /// <summary>
        /// We need to keep some info around across calls - so we use an object to hold it together.
        /// </summary>
        private class ProcessController
        {
            private Process _p;
            LongRunningContext _context;

            internal EPContext Context
            {
                private set;
                get;
            }

            /// <summary>
            /// Start a new process.
            /// </summary>
            /// <param name="info"></param>
            /// <param name="c"></param>
            public ProcessController(ProcessStartInfo info, LongRunningContext c)
            {
                CommonInit(c);
                StartNewProcess(info);
            }

            /// <summary>
            /// The process may still be running -- see if we can restart it.
            /// </summary>
            /// <param name="lastRun"></param>
            public ProcessController(EPContext lastRun, ProcessStartInfo info, LongRunningContext c, bool canRestart)
            {
                CommonInit(c);

                ///
                /// See if that other process is still running
                /// 

                try
                {
                    _p = Process.GetProcessById(lastRun.ID);
                    Trace.WriteLine("Found old processing hanging around");
                    WireUpProcessEvents();
                    CreateContext();
                    return;
                }
                catch (ArgumentException e)
                {
                    if (!e.Message.Contains("Process"))
                    {
                        throw e;
                    }
                }

                ///
                /// Ok -- lets see if we can restart the process since we seem to have
                /// missed it the first time.
                /// 

                if (canRestart)
                {
                    StartNewProcess(info);
                }
                else
                {
                    throw new ExternalProgramActivityException("Program crashed while WF host was down and not allowed to restart it!");
                }          
            }

            /// <summary>
            /// Common init code
            /// </summary>
            /// <param name="c"></param>
            private void CommonInit(LongRunningContext c)
            {
                _context = c;
            }

            /// <summary>
            /// Do the dirty work of starting a process.
            /// </summary>
            /// <param name="info"></param>
            private void StartNewProcess(ProcessStartInfo info)
            {
                Trace.WriteLine("Starting a new process");
                _p = new Process();
                _p.StartInfo = info;
                WireUpProcessEvents();

                if (!_p.Start())
                {
                    throw new ExternalProgramActivityException("Failed to start the process");
                }

                CreateContext();
            }

            /// <summary>
            /// We want to catch the exit..
            /// </summary>
            private void WireUpProcessEvents()
            {
                _p.EnableRaisingEvents = true;
                _p.Exited += new EventHandler(_p_Exited);
            }

            /// <summary>
            /// Save the context of the process that we can use to re-start if we have to.
            /// </summary>
            private void CreateContext()
            {
                Context = new EPContext();
                Context.ID = _p.Id;
            }
            
            /// <summary>
            /// Process is done. Let everyone know!
            /// </summary>
            /// <param name="sender"></param>
            /// <param name="e"></param>
            void _p_Exited(object sender, EventArgs e)
            {
                EPResult r = new EPResult();
                r._result = _p.ExitCode;
                _context.Complete(r);
            }
        }

        /// <summary>
        /// Start the external program running.
        /// 
        /// 1) If this is the first time we are called, we will start the program.
        /// 2) If the program is still running, we will continue to wait on it.
        /// 3) If the program is not running, and we are allowed to restart it, we will
        /// 4) Throw an exception.
        /// </summary>
        /// <param name="context"></param>
        /// <param name="start"></param>
        /// <param name="lastTime"></param>
        /// <returns></returns>
        [LongRunningMethodStarter]
        static public EPContext RunExternalProgram(LongRunningContext context, EPStartupInfo start, EPContext lastTime)
        {
            ProcessController pc = null;
            ProcessStartInfo pinfo = new ProcessStartInfo(start._path);
            pinfo.Arguments = start._arguments;
            pinfo.CreateNoWindow = false;
            pinfo.ErrorDialog = false;
            pinfo.UseShellExecute = false;

            if (lastTime == null)
            {
                Trace.WriteLine("Starting a new external program because our context is null");
                pc = new ProcessController(pinfo, context);
            }
            else
            {
                Trace.WriteLine("Attempting to reconnect to old program because we have a context");
                pc = new ProcessController(lastTime, pinfo, context, start._can_restart);
            }
            return pc.Context;
        }

        /// <summary>
        /// Pull together the activity arguments to be sent off to the startup.
        /// </summary>
        /// <returns></returns>
        [LongRunningGatherArguments]
        public EPStartupInfo Gather()
        {
            EPStartupInfo info = new EPStartupInfo();

            info._path = ProgramPath;
            info._arguments = ProgramArguments;
            info._can_restart = AllowRestart;

            return info;
        }

        /// <summary>
        /// Stash the stuff coming back into our local variables before the WF moves onto
        /// the next fellow.
        /// </summary>
        /// <param name="result"></param>
        [LongRunningDistributeArguments]
        public void Distribute(EPResult result)
        {
            FinalStatus = result._result;
        }
	}
}
